/*******************************************************************************
 * CLI - A simple command line interface.
 * Copyright (C) 2016-2021 Daniele Pallastrelli
 *
 * Boost Software License - Version 1.0 - August 17th, 2003
 *
 * Permission is hereby granted, free of charge, to any person or organization
 * obtaining a copy of the software and accompanying documentation covered by
 * this license (the "Software") to use, reproduce, display, distribute,
 * execute, and transmit the Software, and to prepare derivative works of the
 * Software, and to permit third-parties to whom the Software is furnished to
 * do so, all subject to the following:
 *
 * The copyright notices in the Software and this entire statement, including
 * the above license grant, this restriction and the following disclaimer,
 * must be included in all copies of the Software, in whole or in part, and
 * all derivative works of the Software, unless such copies or derivative
 * works are solely in the form of machine-executable object code generated by
 * a source language processor.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
 * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
 * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 ******************************************************************************/

#ifndef CLI_DETAIL_HISTORY_H_
#define CLI_DETAIL_HISTORY_H_

#include <deque>
#include <limits>
#include <vector>
#include <string>
#include <algorithm>
#include <cassert>

namespace cli
{
namespace detail
{

class History
{
public:

    explicit History(std::size_t size) : maxSize(size) {}

    // Insert a new item in the buffer, changing the current state to "inserting"
    // If we're browsing the history (eg with arrow keys) the new item overwrites
    // the current one.
    // Otherwise, the item is added to the front of the container
    void NewCommand(const std::string& item)
    {
        ++commands;
        current = 0;
        if (mode == Mode::browsing)
        {
            assert(!buffer.empty());
            if (buffer.size() > 1 && buffer[1] == item) // try to insert an element identical to last one
                buffer.pop_front();
            else // the item was not identical
                buffer[current] = item;
        }
        else // Mode::inserting
        {
            if (buffer.empty() || buffer[0] != item) // insert an element not equal to last one
                Insert(item);
        }
        mode = Mode::inserting;
    }

    // Return the previous item of the history, updating the current item and
    // changing the current state to "browsing"
    // If we're already browsing the history (eg with arrow keys) the edit line is inserted
    // to the front of the container.
    // Otherwise, the line overwrites the current item.
    std::string Previous(const std::string& line)
    {
        if (mode == Mode::inserting)
        {
            Insert(line);
            mode = Mode::browsing;
            current = (buffer.size() > 1) ? 1 : 0;
        }
        else // Mode::browsing
        {
            assert(!buffer.empty());
            buffer[current] = line;
            if (current != buffer.size()-1)
                ++current;
        }
        assert(mode == Mode::browsing);
        assert(current < buffer.size());
        return buffer[current];
    }

    // Return the next item of the history, updating the current item.
    std::string Next()
    {
        if (buffer.empty() || current == 0)
            return {};
        assert(current != 0);
        --current;
        assert(current < buffer.size());
        return buffer[current];
    }

    // Show the whole history on the given ostream
    void Show(std::ostream& out) const
    {
        const auto size = buffer.size();
        out << '\n';
        for (std::size_t i = 0; i < size; ++i)
        {
            const auto j = size-1-i;
            out << IndexToId(j) << '\t' << buffer[j] << '\n';
        }
        out << '\n' << std::flush;
    }

    // cmds[0] is the oldest command, cmds[size-1] the newer
    void LoadCommands(const std::vector<std::string>& cmds)
    {
        for (const auto& c: cmds)
            Insert(c);
    }

    // result[0] is the oldest command, result[size-1] the newer
    std::vector<std::string> GetCommands() const
    {
        auto numCmdsToReturn = std::min(commands, buffer.size());
        auto start = buffer.begin();
        if (mode == Mode::browsing)
        {
            numCmdsToReturn = std::min(commands, buffer.size()-1);
            start = buffer.begin()+1;
        }
        std::vector<std::string> result(numCmdsToReturn);
        assert(std::distance(start, buffer.end()) >= 0);
        assert(numCmdsToReturn <= static_cast<std::size_t>(std::distance(buffer.end(), start)));
        assert(numCmdsToReturn <= static_cast<unsigned long>(std::numeric_limits<long>::max()));
        std::reverse_copy(start, start+static_cast<long>(numCmdsToReturn), result.begin());
        return result;
    }

    std::string At(std::size_t id) const
    {
        std::size_t index = IdToIndex(id);
        assert(index < buffer.size());
        return buffer[index];
    }

    void ForgetLatest()
    {
        assert(!buffer.empty());
        buffer.pop_front();
    }

private:

    // oldest has index = size-1 and id = idOfOldest
    // newest has index = 0      and id = idOfOldest + size-1 
    std::size_t IndexToId(std::size_t index) const
    {
        if (index > idOfOldest+buffer.size()-1)
            throw std::out_of_range("Index not found in history");
        return idOfOldest + buffer.size() - 1 - index;
    }

    std::size_t IdToIndex(std::size_t id) const
    {
        if (id < idOfOldest || id > idOfOldest+buffer.size()-1)
            throw std::out_of_range("Index not found in history");
        return idOfOldest + buffer.size() - 1 - id;
    }

    void Insert(const std::string& item)
    {
        buffer.emplace_front(item);
        if (buffer.size() > maxSize)
        {
            buffer.pop_back();
            ++idOfOldest;
        }
    }

    const std::size_t maxSize;
    std::deque<std::string> buffer; // buffer[0] is the newest element
    std::size_t current = 0;
    std::size_t commands = 0; // number of commands issued
    enum class Mode { inserting, browsing };
    Mode mode = Mode::inserting;
    std::size_t idOfOldest = 0; // the id of the oldest element kept in the history
};

} // namespace detail
} // namespace cli

#endif // CLI_DETAIL_HISTORY_H_
